S01-04 Servlet-手写Tomcat
[TOC]
基本实现
Tomcat 的本质是 HTTP 服务器 + Servlet 容器。HTTP 服务器负责底层的 Socket 通信与报文解析,Servlet 容器则负责管理业务组件的生命周期并分发调用。
手写实现的核心链路分为两个核心阶段:
- 第一阶段(核心链路原型) 实现基础网络通信、报文解析、Servlet 契约与单线程分发;
- 第二阶段(生产特性扩展) 补充多线程并发模型与配置文件/注解的动态加载。以下为第一阶段的核心实现。
核心架构与处理流程
核心架构:Web 服务器与 Servlet 之间的核心职责边界如下图所示:

处理流程:从浏览器发起请求到接收响应,容器内部遵循以下执行流:
建立通信:服务端通过
ServerSocket监听指定端口,接收客户端的 TCP 连接。报文提取:从 Socket 的
InputStream中读取原始 HTTP 请求文本。对象封装:将原始报文解析并封装为统一的
MyRequest和MyResponse对象。路由派发:根据请求行中的 URI,在映射表中查找匹配的
MyServlet实例。业务执行与回写:调用 Servlet 的
service()方法,将处理结果通过OutputStream遵循 HTTP 报文格式写回客户端。
核心引擎与请求分发@
MyTomcat 承担连接器与容器的核心角色,维持端口监听、维护 URL 与 Servlet 的映射表,并完成请求调度。
// 初始化路由映射表并监听指定端口接收网络连接
public class MyTomcat {
private final int port;
private final Map<String, MyServlet> servletMapping = new HashMap<>();
public MyTomcat(int port) { this.port = port; }
// 注册请求路径与Servlet实例的映射关系
private void initServletMapping() {
servletMapping.put("/user", new UserServlet());
}
// 启动Socket服务监听并按请求路径分发到具体Servlet
public void start() {
initServletMapping();
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("Tomcat running on port " + port);
while (true) {
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
MyRequest request = new MyRequest(inputStream);
MyResponse response = new MyResponse(outputStream);
MyServlet servlet = servletMapping.get(request.getUrl());
if (servlet != null) {
servlet.service(request, response);
} else {
response.write("<h1>404 Not Found</h1>");
}
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}Servlet 规范定义
Servlet 是容器与具体业务逻辑之间的契约规范。通过抽象类定义通用生命周期方法,并依据 HTTP Method 执行方法分发。
// 定义Servlet生命周期与请求处理标准规范
public abstract class MyServlet {
public void init() {}
public void destroy() {}
// 根据请求方法类型分发至对应的doGet或doPost
public void service(MyRequest request, MyResponse response) throws Exception {
if ("POST".equalsIgnoreCase(request.getMethod())) {
doPost(request, response);
} else if ("GET".equalsIgnoreCase(request.getMethod())) {
doGet(request, response);
}
}
public abstract void doGet(MyRequest request, MyResponse response) throws Exception;
public abstract void doPost(MyRequest request, MyResponse response) throws Exception;
}HTTP 请求解析
负责从网络输入流中提取字节数据,解析出 HTTP 请求行中的请求方法(Method)与请求路径(URI)。MyRequest
// 从输入流中读取HTTP原生报文并提取请求行信息
public class MyRequest {
private String method;
private String url;
public MyRequest(InputStream inputStream) throws IOException {
// 读取网络字节流并截取解析首行请求参数
byte[] buffer = new byte[1024];
int len = inputStream.read(buffer);
if (len > 0) {
String content = new String(buffer, 0, len);
String[] lines = content.split("\\n");
if (lines.length > 0) {
String[] requestLine = lines[0].split(" ");
if (requestLine.length >= 2) {
this.method = requestLine[0];
this.url = requestLine[1];
}
}
}
}
public String getMethod() { return method; }
public String getUrl() { return url; }
}HTTP 响应封装
负责封装 HTTP 协议的响应规范,自动追加状态行与必要的消息头,再将响应体通过网络输出流回传。MyResponse
// 组装符合HTTP协议格式的响应头与响应体数据
public class MyResponse {
private final OutputStream outputStream;
public MyResponse(OutputStream outputStream) {
this.outputStream = outputStream;
}
// 拼接HTTP状态行与响应头后将内容回写给客户端浏览器
public void write(String content) throws IOException {
String responseText = "HTTP/1.1 200 OK\r\n"
+ "Content-Type: text/html;charset=UTF-8\r\n"
+ "Content-Length: " + content.getBytes(StandardCharsets.UTF_8).length + "\r\n"
+ "\r\n"
+ content;
outputStream.write(responseText.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
}
}业务实现与测试验证
业务 Servlet 实现
业务开发者只需继承 MyServlet,重写对应的业务处理逻辑:
// 继承Servlet基类实现具体业务逻辑
public class UserServlet extends MyServlet {
@Override
public void doGet(MyRequest request, MyResponse response) throws Exception {
// 处理GET请求并返回响应页面
response.write("<h1>Hello from UserServlet!</h1>");
}
@Override
public void doPost(MyRequest request, MyResponse response) throws Exception {
doGet(request, response);
}
}服务启动与验证
编写主程序入口实例化容器并启动监听:
// 创建Tomcat实例并启动服务
public class Application {
public static void main(String[] args) {
MyTomcat tomcat = new MyTomcat(8080);
// 启动容器接收请求
tomcat.start();
}
}启动程序后,在浏览器访问 http://localhost:8080/user,页面将输出 Hello from UserServlet!;访问未配置的地址如 http://localhost:8080/test,则会收到 404 Not Found 响应。
多线程与 Servlet
并发模型设计
在第一阶段的单线程模型中,主线程调用 socket.getInputStream().read() 和 servlet.service() 时处于完全阻塞状态,后续到来的 TCP 连接只能在操作系统内核队列中排队。
为解决并发请求处理问题,第二阶段引入经典 BIO 的 一连接一线程(Thread-per-connection) 模型。监听主线程仅负责通过 accept() 接收客户端 Socket,每当有新连接建立,立即为其创建一个独立的子线程执行 HTTP 报文解析与业务调度。
如上图所示,服务端为每个客户端 Socket 分配专属线程。在各自的独立线程内完成读取、业务处理与写回操作,多个客户端之间的网络等待与计算相互隔离,实现了基础的并发处理能力。
配置文件规范
为了避免在 Java 代码中硬编码 URL 与具体实现类的绑定关系,引入标准的 web.xml 配置文件。通过解耦别名与路径,建立灵活的路由映射机制。
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<servlet>
<servlet-name>userServlet</servlet-name>
<servlet-class>com.example.UserServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>userServlet</servlet-name>
<url-pattern>/user</url-pattern>
</servlet-mapping>
</web-app>配置映射分为两步协同生效:
组件注册:在
<servlet>标签中指定组件逻辑名称servlet-name与对应的全限定类名servlet-class。路由关联:在
<servlet-mapping>标签中通过相同的servlet-name将外部访问路径url-pattern绑定到该组件。
解析与反射加载
容器启动时,利用 JDK 自带的 JAXP DOM 解析器读取 web.xml 文件,提取标签信息并使用 Java 反射机制实例化 MyServlet,注册到内存映射表中。
// 解析web.xml并利用反射动态构建Servlet映射表
public class WebXmlConfig {
public static Map<String, MyServlet> load(String path) {
Map<String, MyServlet> servletMapping = new HashMap<>();
Map<String, String> nameToClass = new HashMap<>();
Map<String, String> urlToName = new HashMap<>();
try (InputStream is = WebXmlConfig.class.getResourceAsStream(path)) {
// 初始化XML解析器并读取配置文档
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(is);
NodeList servletNodes = doc.getElementsByTagName("servlet");
for (int i = 0; i < servletNodes.getLength(); i++) {
Element ele = (Element) servletNodes.item(i);
String name = ele.getElementsByTagName("servlet-name").item(0).getTextContent();
String clazz = ele.getElementsByTagName("servlet-class").item(0).getTextContent();
nameToClass.put(name, clazz);
}
NodeList mappingNodes = doc.getElementsByTagName("servlet-mapping");
for (int i = 0; i < mappingNodes.getLength(); i++) {
Element ele = (Element) mappingNodes.item(i);
String name = ele.getElementsByTagName("servlet-name").item(0).getTextContent();
String url = ele.getElementsByTagName("url-pattern").item(0).getTextContent();
urlToName.put(url, name);
}
// 根据映射关系通过反射创建Servlet单例并注册
for (Map.Entry<String, String> entry : urlToName.entrySet()) {
String className = nameToClass.get(entry.getValue());
Class<?> clazz = Class.forName(className);
MyServlet servlet = (MyServlet) clazz.getDeclaredConstructor().newInstance();
servletMapping.put(entry.getKey(), servlet);
}
} catch (Exception e) {
e.printStackTrace();
}
return servletMapping;
}
}独立任务封装
为了让工作线程承载单次请求的完整生命周期,将 Socket 的 IO 读取、业务分发及资源关闭动作统一封装为一个实现 Runnable 接口的任务类 SocketProcessor。
// 封装单次客户端Socket请求的处理任务
public class SocketProcessor implements Runnable {
private final Socket socket;
private final Map<String, MyServlet> servletMapping;
public SocketProcessor(Socket socket, Map<String, MyServlet> servletMapping) {
this.socket = socket;
this.servletMapping = servletMapping;
}
@Override
public void run() {
// 在独立线程中完成HTTP解析与Servlet调度
try (
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()
) {
MyRequest request = new MyRequest(inputStream);
MyResponse response = new MyResponse(outputStream);
MyServlet servlet = servletMapping.get(request.getUrl());
if (servlet != null) {
servlet.service(request, response);
} else {
response.write("<h1>404 Not Found</h1>");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 确保处理完成后释放当前Socket网络连接资源
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}容器引擎改造@
重构 MyTomcat 的启动流程:在服务启动时调用 WebXmlConfig.load() 完成配置预加载;在主监听循环中,每当调用 accept() 获取到新连接,直接提交给 new Thread(...) 启动异步处理。
// 支持BIO多线程并发与动态配置加载的Tomcat引擎
public class MyTomcat {
private final int port;
private Map<String, MyServlet> servletMapping = new HashMap<>();
public MyTomcat(int port) { this.port = port; }
public void start() {
// 启动阶段通过反射加载web.xml配置
servletMapping = WebXmlConfig.load("/web.xml");
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("Tomcat BIO Server started on port " + port);
while (true) {
Socket socket = serverSocket.accept();
// 为每个新连接创建并启动专属工作线程
new Thread(new SocketProcessor(socket, servletMapping)).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}架构对比分析
引入多线程与配置文件后,容器在解耦度与并发能力上有了质的提升:
| 对比维度 | 第一阶段(原型) | 第二阶段(BIO 多线程 + web.xml) |
|---|---|---|
| 并发处理能力 | 单线程串行阻塞,后续请求排队 | 一连接一线程,多客户端并行处理 |
| 路由绑定方式 | 源代码硬编码实例,改动需重编译 | web.xml 声明式配置,全限定名反射解耦 |
| Servlet 生命周期 | 容器启动时固定创建对象 | 启动时解析 XML 动态按需或批量反射实例化 |
| 资源隔离性 | 任意单次慢请求导致全局不可用 | 慢请求仅阻塞自身子线程,不影响新连接接入 |
BIO 并发瓶颈分析
虽然 new Thread(...) 满足了多客户端同时请求的基本诉求,但在高并发场景下暴露出严重的架构弊端:
线程创建开销巨大:操作系统创建和销毁内核线程的系统调用代价很高,请求激增时 CPU 消耗主要浪费在线程生命周期管理上。
上下文切换开销剧增:并发线程数远超 CPU 核心数时,线程调度器频繁保存与恢复寄存器上下文,导致系统有效吞吐量骤降。
内存与系统崩溃风险:Java 线程栈默认占用较大内存(如 1MB),无上限地创建线程极易耗尽系统内存导致
OutOfMemoryError,或触发操作系统的最大进程数/文件描述符限制。
上述缺陷促使 Tomcat 在后续演进中采用线程池模型(伪异步 IO),并最终全面转向基于 NIO 的多路复用模型(NioEndpoint)。